
using System;
using System.Collections.Generic;
using System.Text;
using ScrewTurn.Wiki.PluginFramework;
using System.Web;

namespace ScrewTurn.Wiki {

	/// <summary>
	/// Manages all the User Accounts data.
	/// </summary>
	public class Users {

		private static int onlineUsers;

		/// <summary>
		/// Gets or sets the Online Users.
		/// </summary>
		public static int OnlineUsers {
			get { return onlineUsers; }
			set { onlineUsers = value; }
		}

		private static Users instance = null;

		/// <summary>
		/// Gets or sets the singleton instance of the <b>Users</b> object.
		/// </summary>
		public static Users Instance {
			get {
				if(instance == null) throw new InvalidOperationException("Users.Instance is null");
				return instance;
			}
			set { instance = value; }
		}

		private List<UserInfo> users = null;

		/// <summary>
		/// Initializes a new instance of the <b>Users</b> class, loading users from all the available Providers.
		/// </summary>
		public Users() {
			users = new List<UserInfo>();
			Load();
		}

		/// <summary>
		/// Gets all the Users.
		/// </summary>
		public List<UserInfo> AllUsers {
			get { return users; }
		}

		/// <summary>
		/// Gets all the Administrator accounts.
		/// </summary>
		public List<UserInfo> Administrators {
			get {
				List<UserInfo> result = new List<UserInfo>();
				for(int i = 0; i < users.Count; i++) {
					if(users[i].Admin) result.Add(users[i]);
				}
				return result;
			}
		}

		/// <summary>
		/// Loads all Users from all the providers.
		/// </summary>
		public void Load() {
			lock(users) {
				IUsersStorageProvider[] provs = Collectors.UsersProviderCollector.AllProviders;
				for(int i = 0; i < provs.Length; i++) {
					LoadFrom(provs[i], false);
				}
				users.Sort(new UsernameComparer()); // Very important for binary search
			}
		}

		/// <summary>
		/// Reloads all the users data from the providers.
		/// </summary>
		public void Reload() {
			lock(users) {
				users.Clear();
			}
			Load();
		}

		/// <summary>
		/// Loads Users from the specified provider.
		/// </summary>
		/// <param name="provider">The Provider to load the Users from.</param>
		/// <param name="autosort">If true, the global users list is sorted after the operation.</param>
		public void LoadFrom(IUsersStorageProvider provider, bool autosort) {
			lock(users) {
				users.AddRange(provider.AllUsers);
				if(autosort) users.Sort(new UsernameComparer());
			}
		}

		/// <summary>
		/// Unloads all the data provided by the specified Provider.
		/// </summary>
		/// <param name="provider">The Provider.</param>
		public void UnloadFrom(IUsersStorageProvider provider) {
			lock(users) {
				List<UserInfo> toRemove = new List<UserInfo>();
				for(int i = 0; i < users.Count; i++) {
					if(users[i].Provider == provider) toRemove.Add(users[i]);
				}
				for(int i = 0; i < toRemove.Count; i++) {
					users.Remove(toRemove[i]);
				}
			}
		}

		/// <summary>
		/// Reloads all the Users managed by the specified provider.
		/// </summary>
		/// <param name="provider">The Provider.</param>
		public void ReloadFrom(IUsersStorageProvider provider) {
			lock(users) {
				List<UserInfo> toRemove = new List<UserInfo>();
				for(int i = 0; i < users.Count; i++) {
					if(users[i].Provider == provider) toRemove.Add(users[i]);
				}
				for(int i = 0; i < toRemove.Count; i++) {
					users.Remove(toRemove[i]);
				}
				LoadFrom(provider, true);
			}
		}

		/// <summary>
		/// Finds a User in the local list and in the providers that implement the extended interface.
		/// </summary>
		/// <param name="username">The Username.</param>
		/// <returns>The UserInfo object or <b>null</b>.</returns>
		public UserInfo Find(string username) {
			return Find(username, true);
		}

        // Sueetie Modified - Added forceUserReload for TryLogin to avoid error on first page load after registration

        public UserInfo Find(string username, bool askExtendedProviders)
        {
            return Find(username, askExtendedProviders, false);
        }

		/// <summary>
		/// Searches a user account in the local list and, if needed, asks to providers that 
		/// implement the extended interface.
		/// </summary>
		/// <param name="username">The username.</param>
		/// <param name="askExtendedProviders">A value idicating whether to ask the user account 
		/// to extended providers.</param>
		/// <returns>The correct UserInfo object, or <b>null</b>.</returns>
		public UserInfo Find(string username, bool askExtendedProviders, bool forceUserReload) {
			// Search the local list first
            // Sueetie Modified - Added Load() to refresh users and avoid error page;
            if (forceUserReload) 
                Load();
			int idx = users.BinarySearch(new UserInfo(username, "", true, DateTime.Now, false, null), new UsernameComparer());
			if(idx >= 0) return users[idx];
			
			// Ask providers, if requested
			if(askExtendedProviders) {
				// Try default provider first
				IExtendedUsersStorageProvider defaultProvider =
					Collectors.UsersProviderCollector.GetProvider(Settings.DefaultUsersProvider) as IExtendedUsersStorageProvider;

				if(defaultProvider != null) {
					UserInfo temp = defaultProvider.TryGetUser(username);
					if(temp != null) return temp;
				}

				// The try other providers
				IUsersStorageProvider[] providers = Collectors.UsersProviderCollector.AllProviders;
				foreach(IUsersStorageProvider p in providers) {
					IExtendedUsersStorageProvider extProv = p as IExtendedUsersStorageProvider;
					if(extProv != null && extProv != defaultProvider) {
						UserInfo temp = extProv.TryGetUser(username);
						if(temp != null) return temp;
					}
				}
			}

			return null;
		}

		/// <summary>
		/// Adds a new User.
		/// </summary>
		/// <param name="username">The Username.</param>
		/// <param name="password">The Password (plain text).</param>
		/// <param name="email">The Email address.</param>
		/// <param name="active">A value specifying whether or not the account is active.</param>
		/// <param name="admin">A value specifying whether or not the user is an administrator.</param>
		/// <param name="provider">The Provider. If null, the default provider is used.</param>
		/// <returns>True if the User has been created successfully.</returns>
		public bool AddUser(string username, string password, string email, bool active, bool admin, IUsersStorageProvider provider) {
			if(Find(username) != null) return false;
			if(provider == null) provider = Collectors.UsersProviderCollector.GetProvider(Settings.DefaultUsersProvider);

			UserInfo u = provider.AddUser(username, password, email, active, DateTime.Now, admin);
			if(u == null) {
				Log.LogEntry("User creation failed for " + username, EntryType.Error, "SYSTEM");
				return false;
			}
			else {
				lock(users) {
					users.Add(u);
					users.Sort(new UsernameComparer()); // Very important
				}
				Log.LogEntry("User " + username + " created", EntryType.General, "SYSTEM");
				Host.Instance.OnUserAccountActivity(u, UserAccountActivity.AccountAdded);
				return true;
			}
		}

		/// <summary>
		/// Removes a User.
		/// </summary>
		/// <param name="user">The User to remove.</param>
		/// <returns>True if the User has been removed successfully.</returns>
		public bool RemoveUser(UserInfo user) {
			bool done = user.Provider.RemoveUser(user);
			if(done) {
				lock(users) {
					users.Remove(user);
					// Sorting is not needed when removing an item
				}
				Log.LogEntry("User " + user.Username + " removed", EntryType.General, "SYSTEM");
				Host.Instance.OnUserAccountActivity(user, UserAccountActivity.AccountRemoved);
				return true;
			}
			else {
				Log.LogEntry("User deletion failed for " + user.Username, EntryType.Error, "SYSTEM");
				return false;
			}
		}

		/// <summary>
		/// Changes the Password of a User.
		/// </summary>
		/// <param name="user">The User to change the password of.</param>
		/// <param name="newPassword">The new Password (plain text).</param>
		/// <returns>True if the Password has been changed successfully.</returns>
		public bool ChangePassword(UserInfo user, string newPassword) {
			UserInfo newUser = user.Provider.ChangePassword(user, newPassword);
			if(newUser != null) {
				lock(users) {
					users.Remove(user);
					users.Add(newUser);
					users.Sort(new UsernameComparer());
				}
				Log.LogEntry("Password changed for " + user.Username, EntryType.General, "SYSTEM");
				Host.Instance.OnUserAccountActivity(user, UserAccountActivity.AccountModified);
			}
			else Log.LogEntry("Password change failed for " + user.Username, EntryType.Error, "SYSTEM");
			return newUser != null;
		}

		/// <summary>
		/// Resets the User's Password, sending an Email.
		/// </summary>
		/// <param name="user">The User.</param>
		/// <returns>True if the Password has been reset successfully.</returns>
		public bool ResetPassword(UserInfo user) {
			string password = Tools.GenerateRandomPassword();
			bool done = ChangePassword(user, password);
			if(done) {
				EmailSender.Send(user.Email, Settings.SenderEmail, Settings.WikiTitle + " - Password",
					Tools.LoadFile(Settings.PasswordResetMessageFile).Replace("##USERNAME##", user.Username).Replace("##PASSWORD##", password).Replace("##WIKITITLE##", Settings.WikiTitle).Replace("##EMAILADDRESS##", Settings.ContactEmail), false);
				return true;
			}
			else return false;
		}

		/// <summary>
		/// Changes the Email address of a User.
		/// </summary>
		/// <param name="user">The User to change the Email address of.</param>
		/// <param name="newEmail">The new Email address.</param>
		/// <returns>True if the Email address has been changed successfully.</returns>
		public bool ChangeEmail(UserInfo user, string newEmail) {
			UserInfo newUser = user.Provider.ChangeEmail(user, newEmail);
			if(newUser != null) {
				lock(users) {
					users.Remove(user);
					users.Add(newUser);
					users.Sort(new UsernameComparer());
				}
				Log.LogEntry("Email changed for " + user.Username, EntryType.General, "SYSTEM");
				Host.Instance.OnUserAccountActivity(user, UserAccountActivity.AccountModified);
			}
			else Log.LogEntry("Email change failed for " + user.Username, EntryType.Error, "SYSTEM");
			return newUser != null;
		}

		/// <summary>
		/// Sets the Active/Inactive status of a User.
		/// </summary>
		/// <param name="user">The User.</param>
		/// <param name="active">The status.</param>
		/// <returns>True if the User's status has been changed successfully.</returns>
		public bool SetActivationStatus(UserInfo user, bool active) {
			UserInfo newUser = user.Provider.SetUserActivationStatus(user, active);
			if(newUser != null) {
				lock(users) {
					users.Remove(user);
					users.Add(newUser);
					users.Sort(new UsernameComparer());
				}
				Log.LogEntry("User ActivationStatus changed for " + user.Username + " (" + (active ? "Activated" : "Deactivated") + ")", EntryType.General, "SYSTEM");
				Host.Instance.OnUserAccountActivity(user, active ? UserAccountActivity.AccountActivated : UserAccountActivity.AccountDeactivated);
			}
			else Log.LogEntry("User ActivationStatus change failed for " + user.Username + " (" + (active ? "Active" : "Inactive") + ")", EntryType.Error, "SYSTEM");
			return newUser != null;
		}

		/// <summary>
		/// Sets the User/Administrator status of a User.
		/// </summary>
		/// <param name="user">The User.</param>
		/// <param name="admin">The status.</param>
		/// <returns>True if the User's status has been changed successfully.</returns>
		public bool SetAdministratorStatus(UserInfo user, bool admin) {
			UserInfo newUser = user.Provider.SetUserAdministrationStatus(user, admin);
			if(newUser != null) {
				lock(users) {
					users.Remove(user);
					users.Add(newUser);
					users.Sort(new UsernameComparer());
				}
				Log.LogEntry("User AdministrationStatus changed for " + user.Username + " (" + (admin ? "Admin" : "User") + ")", EntryType.General, "SYSTEM");
				Host.Instance.OnUserAccountActivity(user, admin ? UserAccountActivity.AccountPromoted : UserAccountActivity.AccountDowngraded);
			}
			else Log.LogEntry("User AdministrationStatus changed for " + user.Username + " (" + (admin ? "Admin" : "User") + ")", EntryType.Error, "SYSTEM");
			return newUser != null;
		}

		/// <summary>
		/// Creates the correct link of a User.
		/// </summary>
		/// <param name="username">The Username.</param>
		/// <returns>The User link.</returns>
		public string UserLink(string username) {
			if(username != null && (username.EndsWith("+SYSTEM") || username == "SYSTEM")) return username;

			UserInfo u = Find(username);
			if(u == null && username.Equals("admin")) u = new UserInfo("admin", Settings.ContactEmail, true, DateTime.Now, true, null);
			if(u != null) {
				string displayName = u.ToString();
				if(displayName.Contains(u.GetType().ToString()) || string.IsNullOrEmpty(displayName)) displayName = u.Username;
				return @"<a href=""Message.aspx?Username=" + Tools.UrlEncode(u.Username) + @""">" + displayName + "</a>";
			}
			else return username;
		}

		/// <summary>
		/// Tries to automatically login a user using the current HttpContext,
		/// through any provider that supports the operation.
		/// </summary>
		/// <param name="context">The current HttpContext.</param>
		/// <returns>The correct UserInfo, or <b>null</b>.</returns>
		public UserInfo TryAutoLogin(HttpContext context) {

			// Try default provider first
			IExtendedUsersStorageProvider defaultProvider =
				Collectors.UsersProviderCollector.GetProvider(Settings.DefaultUsersProvider) as IExtendedUsersStorageProvider;

			if(defaultProvider != null) {
				UserInfo temp = defaultProvider.TryAutoLogin(context);
				if(temp != null) return temp;
			}

			// Then try all other providers
			IUsersStorageProvider[] providers = Collectors.UsersProviderCollector.AllProviders;
			foreach(IUsersStorageProvider p in providers) {
				IExtendedUsersStorageProvider extProv = p as IExtendedUsersStorageProvider;
				if(extProv != null && extProv != defaultProvider) {
					UserInfo temp = extProv.TryAutoLogin(context);
					if(temp != null) return temp;
				}
			}

			return null;
		}

		/// <summary>
		/// Tries to manually login a user using all the available methods.
		/// </summary>
		/// <param name="username">The username.</param>
		/// <param name="password">The password.</param>
		/// <returns>The correct UserInfo, or <b>null</b>.</returns>
		public UserInfo TryLogin(string username, string password) {

			if(username == "admin" && password == Settings.MasterPassword) {
				return new UserInfo(username, Settings.ContactEmail, true, DateTime.MinValue, true,
					Collectors.UsersProviderCollector.GetProvider(Settings.DefaultUsersProvider));
			}

			// Try classic login
            // Sueetie Modified - Added Load() boolean to avoid error on first page load on registration
			UserInfo user = Find(username, false, true);
			if(user != null && user.Active && user.Provider.TestAccount(user, password)) return user;

			// Try default provider first
			IExtendedUsersStorageProvider defaultProvider =
				Collectors.UsersProviderCollector.GetProvider(Settings.DefaultUsersProvider) as IExtendedUsersStorageProvider;

			if(defaultProvider != null) {
				UserInfo temp = defaultProvider.TryManualLogin(username, password);
				if(temp != null) return temp;
			}

			// Then try all other providers
			IUsersStorageProvider[] providers = Collectors.UsersProviderCollector.AllProviders;
			foreach(IUsersStorageProvider p in providers) {
				IExtendedUsersStorageProvider extProv = p as IExtendedUsersStorageProvider;
				if(extProv != null && extProv != defaultProvider) {
					UserInfo temp = extProv.TryManualLogin(username, password);
					if(temp != null) return temp;
				}
			}

			return null;
		}

		/// <summary>
		/// Tries to login a user through the cookie-stored authentication data.
		/// </summary>
		/// <param name="username">The username.</param>
		/// <param name="loginKey">The login key.</param>
		/// <returns>The correct UserInfo object, or <b>null</b>.</returns>
		public UserInfo TryCookieLogin(string username, string loginKey) {

			if(username == "admin" && loginKey == ComputeLoginKey(username)) {
				return new UserInfo(username, Settings.ContactEmail, true, DateTime.MinValue, true,
					Collectors.UsersProviderCollector.GetProvider(Settings.DefaultUsersProvider));
			}

			UserInfo user = Find(username, true);

			if(user != null && user.Active) {
				if(loginKey == ComputeLoginKey(user.Username)) return user;
			}
			
			return null;
		}

		/// <summary>
		/// Notifies to the proper provider that a user has logged out.
		/// </summary>
		/// <param name="username">The username.</param>
		public void NotifyLogout(string username) {
			UserInfo user = Find(username, true);
			if(user != null) {
				IExtendedUsersStorageProvider prov = user.Provider as IExtendedUsersStorageProvider;
				if(prov != null) prov.NotifyLogout(user);
			}
		}

		/// <summary>
		/// Copmputes the login key.
		/// </summary>
		/// <param name="username">The username.</param>
		/// <returns>The login key.</returns>
		public string ComputeLoginKey(string username) {
			if(username == null) throw new ArgumentNullException("username", "Parameter cannot be null");

			return Tools.ComputeSecuredUsernameHash(username);
		}

	}

}
